package com.air.ota.net;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.air.ota.util.HexStringUtil;


public class NIOClient implements Runnable{
	
	private InetAddress host;
	private int			port;
	
	private Selector selector;
	private ByteBuffer readBuffer = ByteBuffer.allocate(512);
	private List<ChangeRequest > pendingChanges = new LinkedList<ChangeRequest>();
	
	private HashMap<SocketChannel, List<ByteBuffer>> pendingData =new HashMap<SocketChannel, List<ByteBuffer>>();
	
	private Map rspHandlers = Collections.synchronizedMap(new HashMap());
	
	public NIOClient(InetAddress hostAddress, int port) throws IOException {
		this.host = hostAddress;
		this.port = port;
		this.selector = this.initSelector();
	}
	
	public void send(byte[] data, ResponseHandler handler) throws IOException {
		// Start a new connection
		SocketChannel socket = this.initiateConnection();
		
		// Register the response handler
		this.rspHandlers.put(socket, handler);
		
		// And queue the data we want written
		synchronized (this.pendingData) {
			List queue = (List) this.pendingData.get(socket);
			if (queue == null) {
				queue = new ArrayList();
				this.pendingData.put(socket, queue);
			}
			queue.add(ByteBuffer.wrap(data));
		}

		// Finally, wake up our selecting thread so it can make the required changes
		this.selector.wakeup();
	}

	public void run() {
		while (true) {
			try {
				// Process any pending changes
				synchronized (this.pendingChanges) {
					Iterator changes = this.pendingChanges.iterator();
					while (changes.hasNext()) {
						ChangeRequest change = (ChangeRequest) changes.next();
						switch (change.type) {
						case ChangeRequest.CHANGEOPS:
							SelectionKey key = change.socket.keyFor(this.selector);
							key.interestOps(change.ops);
							break;
						case ChangeRequest.REGISTER:
							change.socket.register(this.selector, change.ops);
							break;
						}
					}
					this.pendingChanges.clear();
				}

				// Wait for an event one of the registered channels
				this.selector.select();

				// Iterate over the set of keys for which events are available
				Iterator selectedKeys = this.selector.selectedKeys().iterator();
				while (selectedKeys.hasNext()) {
					SelectionKey key = (SelectionKey) selectedKeys.next();
					selectedKeys.remove();

					if (!key.isValid()) {
						continue;
					}

					// Check what event is available and deal with it
					if (key.isConnectable()) {
						this.finishConnection(key);
					} else if (key.isReadable()) {
						this.read(key);
					} else if (key.isWritable()) {
						this.write(key);
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	private void read(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();

		// Clear out our read buffer so it's ready for new data
		this.readBuffer.clear();

		// Attempt to read off the channel
		int numRead;
		try {
			numRead = socketChannel.read(this.readBuffer);
		} catch (IOException e) {
			// The remote forcibly closed the connection, cancel
			// the selection key and close the channel.
			key.cancel();
			socketChannel.close();
			return;
		}

		if (numRead == -1) {
			// Remote entity shut the socket down cleanly. Do the
			// same from our end and cancel the channel.
			key.channel().close();
			key.cancel();
			return;
		}

		// Handle the response
		this.handleResponse(socketChannel, this.readBuffer.array(), numRead);
	}

	private void handleResponse(SocketChannel socketChannel, byte[] data, int numRead) throws IOException {
		// Make a correctly sized copy of the data before handing it
		// to the client
		byte[] rspData = new byte[numRead];
		System.arraycopy(data, 0, rspData, 0, numRead);
		
		// Look up the handler for this channel
		ResponseHandler handler = (ResponseHandler) this.rspHandlers.get(socketChannel);
		
		// And pass the response to it
		if (handler.handleResponse(rspData)) {
			// The handler has seen enough, close the connection
			socketChannel.close();
			socketChannel.keyFor(this.selector).cancel();
		}
	}

	private void write(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();

		synchronized (this.pendingData) {
			List queue = (List) this.pendingData.get(socketChannel);

			// Write until there's not more data ...
			while (!queue.isEmpty()) {
				ByteBuffer buf = (ByteBuffer) queue.get(0);
				socketChannel.write(buf);
				if (buf.remaining() > 0) {
					// ... or the socket's buffer fills up
					break;
				}
				queue.remove(0);
			}

			if (queue.isEmpty()) {
				// We wrote away all data, so we're no longer interested
				// in writing on this socket. Switch back to waiting for
				// data.
				key.interestOps(SelectionKey.OP_READ);
			}
		}
	}

	private void finishConnection(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();
	
		// Finish the connection. If the connection operation failed
		// this will raise an IOException.
		try {
			socketChannel.finishConnect();
		} catch (IOException e) {
			// Cancel the channel's registration with our selector
			System.out.println(e);
			key.cancel();
			return;
		}
	
		// Register an interest in writing on this channel
		key.interestOps(SelectionKey.OP_WRITE);
	}

	private SocketChannel initiateConnection() throws IOException {
		// Create a non-blocking socket channel
		SocketChannel socketChannel = SocketChannel.open();
		socketChannel.configureBlocking(false);
	
		// Kick off connection establishment
		socketChannel.connect(new InetSocketAddress(this.host, this.port));
	
		// Queue a channel registration since the caller is not the 
		// selecting thread. As part of the registration we'll register
		// an interest in connection events. These are raised when a channel
		// is ready to complete connection establishment.
		synchronized(this.pendingChanges) {
			this.pendingChanges.add(new ChangeRequest(socketChannel, ChangeRequest.REGISTER, SelectionKey.OP_CONNECT));
		}
		
		return socketChannel;
	}

	private Selector initSelector() throws IOException {
		// Create a new selector
		return SelectorProvider.provider().openSelector();
	}

	public static void main(String[] args) {
		//String msg ="00A100000090000100000000000000000000000000000030010111E1A2FF00000002201104211100000000000080001122000000006F53006C9133C8F7ECA1D125F689B386C631E87D0112020151800000811D2C0411223344000088888800000000FF11223344556620110420112200E8030000E80300008000000000000000271020110120090000202101200900000000000000000000000000001C161F53BB0000";
		//byte[] msgBytes = HexStringUtil.hexStringToBytes(msg);
		
		
		
		
		System.out.println("aaa");
		
		/*
		String bodyAsc = "30010111E1A2FF0000000220110421110000000000008000112200000000E38C5490A2B7913DECA41C18723441ECBC5838BD0112020151800000811D2C0411223344000088888800000000FF11223344556620110420112200E8030000E80300008000000000000000271020110120090000202101200900000000000000000000000000001C38157487";
		String bodyHex  = HexStringUtil.asc2Hex(bodyAsc);
		System.out.println(bodyAsc.length());
		System.out.println(bodyHex.length());
		System.out.println(bodyHex);
		
		
		String headHex = "00000090000100000000000000000000000000";
		String macHex="0000";
		int msgLength = (headHex+bodyHex+macHex).length()/2;
		
		
		System.out.println("b="+HexStringUtil.hexStringToBytes(headHex+bodyHex+macHex).length);
		
		//String msgHex ="0"+Integer.toHexString(msgLength)+headHex+bodyHex+macHex;
		
		String msgHex ="2901"+headHex+bodyHex+macHex;
		
		System.out.println(msgHex);
		byte[] msgBytes = HexStringUtil.hexStringToBytes(msgHex);
		
		System.out.println(msgBytes.length);*/
		
		String msg = "2901000000900001000000000000000000000000003330303130313131653161326666303030303030303132303131303530333136323530373030303030303030383030303131323230303030303030303139343834396466373535376532623837343765356530323765393337323035313138363536616230313132303230313531383030303030383131643263303430303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303032303131303530333136323530373230323130353033313632353037303030303030303030303030303030303030303030303030316362306230346163320000";
		byte[] msgBytes = HexStringUtil.hexStringToBytes(msg);
		
		try {
			NIOClient client = new NIOClient(InetAddress.getByName("121.15.13.60"), 9000);
			//NIOClient client = new NIOClient(InetAddress.getByName("127.0.0.1"), 9000);
			Thread t = new Thread(client);
			t.setDaemon(true);
			t.start();
			ResponseHandler handler = new ResponseHandler();
			client.send(msgBytes, handler);
			System.out.println("sent");
			handler.wait4Response();
		} catch (Exception e) {	
			e.printStackTrace();
		}
	}
	
	
}
